package com.gitee.chili.leisure.handler;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.gitee.chili.leisure.Constants;
import com.gitee.chili.leisure.RedisKeys;
import com.gitee.chili.leisure.RoomManager;
import com.gitee.chili.leisure.SimpleBeanFactory;
import com.gitee.chili.leisure.base.RedisService;
import com.gitee.chili.leisure.constant.Constant;
import com.gitee.chili.leisure.core.Packet;
import com.gitee.chili.leisure.domain.BallotBox;
import com.gitee.chili.leisure.domain.dto.RequestModel;
import com.gitee.chili.leisure.enums.CardType;
import com.gitee.chili.leisure.enums.GameType;
import com.gitee.chili.leisure.enums.State;
import com.gitee.chili.leisure.exception.BusinessException;
import com.gitee.chili.leisure.exception.GameNotFoundException;
import com.gitee.chili.leisure.exception.TimerTaskNotFoundException;
import com.gitee.chili.leisure.game.MahjongPlayer;
import com.gitee.chili.leisure.game.Player;
import com.gitee.chili.leisure.game.RoomDetail;
import com.gitee.chili.leisure.message.ExchangeThree;
import com.gitee.chili.leisure.model.MahjongModel;
import com.gitee.chili.leisure.model.MessageCmdEnum;
import com.gitee.chili.leisure.properties.TimeoutProperties;
import com.gitee.chili.leisure.service.IGameHandleService;
import com.gitee.chili.leisure.service.SpecificTaskTransferClient;
import com.gitee.chili.leisure.util.Enum;
import com.gitee.chili.leisure.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

/**
 * 消息中转处理器，这时消息已经被初始化，并且将要发出
 *
 * @author week solider
 * @since 2020.09.14
 */
@Slf4j
@Component
public class MessageTransferHandler {


    @Autowired
    private RedisService redisService;

    @Autowired
    private IGameHandleService handleService;

    @Autowired
    private TimeoutProperties timeoutProperties;

    /**
     * 开始投票解散房间任务
     *
     * @param roomNum 房间号
     */
    public void voteDissolvedRoomTask(Long roomNum) {

        // 定时任务的名称
        String name = String.format(TaskManager.TaskNames.voteDissolvedName, roomNum.toString());

        // 如果已经存在这个定时器则不需要提交任务
        if (TaskManager.contains(name))
            return;

        // 投票解散定时任务
        // 60秒后房间还没有选举出结果则解散
        Timer timer = TaskScheduler.submit(() -> {
            // 解散房间
            SpecificTaskTransferClient client = SimpleBeanFactory.getBean(SpecificTaskTransferClient.class);
            try {
                client.dissolvedRoom(roomNum);
            } catch (Exception e) {
                log.error("解散房间写入失败, 房间号: {} , errMsg: {}", roomNum, e.getMessage(), e);
            }

            // 广播房间解散
            Connection.broadcaster().notify(roomNum, AuxiliaryRoom.dissolved(roomNum));

        }, timeoutProperties.getVoteTimeout());

        // 添加到任务管理器
        TaskManager.set(name, timer);
    }


    /**
     * 取消投票定时任务
     *
     * @param roomNum 房间号
     */
    public void cancelVoteDissolvedTask(Long roomNum) {

        String key = String.format(RedisKeys.voteDissolved, roomNum.toString());

        // 房间人数
        int peopleNum = RoomManager.obtain(roomNum).getPeopleNum();

        // 满足条件 定时任务不需要执行
        if (redisService.getHash(key, roomNum) >= peopleNum) {
            cancel(String.format(TaskManager.TaskNames.voteDissolvedName, roomNum.toString()));
        }
    }

    /**
     * 记录一个投票信息
     */
    public void recordVote(BallotBox box) {
        // 投票人头统计
        String key = String.format(RedisKeys.voteDissolved, box.getRoomNum());
        redisService.incrementHash(key, box.getRoomNum());

        // 投票结果记录
        String voteResult = String.format(RedisKeys.roomVoteResult, box.getRoomNum());
        redisService.setHash(voteResult, box.getMemberId(), box.getIsAgree().toString());
    }

    /**
     * 开始游戏
     */
    public void start(Long roomNum) {

        String name = String.format(TaskManager.TaskNames.room, roomNum.toString());

        // 设置房间不过期
        TaskManager.clear(name);

        // 检查房间缓存信息
        verificationRoom(roomNum);

        // 广播开始游戏
        Connection.broadcaster().notify(roomNum, AuxiliaryRoom.begin());

        RoomDetail roomDetail = RoomManager.obtain(roomNum);

        /*
            XXX 请求游戏服务, 这里需要根据游戏类型自动分发到游戏服务
            XXX 目前没有更多的游戏, 这里固定为mahjong
            XXX 更好的解决方案是用多态的方式自动识别到游戏类型
         */
        String prePath = parsePath(roomDetail.getGameType());

        try {

            // 请求参数
            Supplier<RequestModel> supplier = () -> RequestModel.builder()
                    .data(JSONUtil.createObj().set("roomNum", roomNum))
                    .url(Utils.connect(prePath, "/begin"))
                    .build();

            // 开始游戏, 这里的工序包含了开始游戏前的所有工作
            handleService.request(supplier, response -> roomDetail.getPlayers().forEach(p -> {
                // 更新游戏状态
                p.setStatus(State.gaming);

                // 推送发牌数据
                Connection.broadcaster().send(roomNum, p.getUserId(), AuxiliaryRoom.pushDealData(roomNum, p.getUserId()));

                /*
                    不可以同时触发，当前阶段还只是处在通知阶段，两个阶段不能同时开始，故需要先进行一个
                    如果先进行了换三张，那么还需要再定缺
                 */
                if (roomDetail.getRules().getChangeThree() == Constants.OPEN) {
                    // 通知开始换三张
                    Connection.broadcaster().notify(roomNum, AuxiliaryRoom.launchExchange());
                    // 换三张定时任务
                    exchangeThreeTask(roomNum, (MahjongPlayer) p);
                } else {
                    // 通知开始定缺
                    Connection.broadcaster().notify(roomNum, AuxiliaryRoom.launchUseless());
                    // 定缺定时任务
                    uselessTask(roomNum, (MahjongPlayer) p);
                }
            }));

        } catch (Exception e) {
            log.error("房间 {} 开始游戏 --> 请求游戏服务器失败 errMsg: {}", roomNum, e.getMessage(), e);
            throw new BusinessException("请求游戏服务器失败, 房间号: " + roomNum);
        }
    }

    /**
     * 定缺定时任务
     *
     * @param roomNum 房间号
     * @param player  玩家信息
     */
    public void uselessTask(Long roomNum, MahjongPlayer player) {

        String name = String.format(TaskManager.TaskNames.multiUseless, roomNum.toString());

        Timer timer = TaskScheduler.submit(() -> {

            CardType type = AuxiliaryRoom.optimalExchangeType(player);

            player.setUseless(type);

            RoomDetail roomDetail = RoomManager.obtain(roomNum);

            List<? extends Player> players = roomDetail.getPlayers();

            // 定缺结束
            if (AuxiliaryRoom.uselessOver(players)) {

                // 定缺结束 广播给房间内的玩家
                Connection.broadcaster().notify(roomNum, AuxiliaryRoom.uselessFinish(players));

                // 通知玩家出牌
                Utils.accept(players, p -> p.getPosition() == roomDetail.getBankerSeat(), this::notifiesDiscard);
            }

        }, timeoutProperties.getUselessTimeout());

        MemberTimer memberTimer = MemberTimer.builder()
                .memberId(player.getUserId())
                .timer(timer)
                .build();

        TaskManager.add(name, memberTimer);
    }

    /**
     * 换三张定时任务
     *
     * @param roomNum 房间号
     * @param player  玩家
     */
    public void exchangeThreeTask(Long roomNum, MahjongPlayer player) {

        String multiExchangeThree = String.format(TaskManager.TaskNames.multiExchangeThree, roomNum.toString());

        Timer timer = TaskScheduler.submit(() -> {

            // 已经确定了换三张的牌 (结束)
            if (!player.getExchange().isEmpty()) {
                return;
            }

            List<Integer> exchange = new ArrayList<>();

            // 最优的换牌类型
            CardType exchangeType = AuxiliaryRoom.optimalExchangeType(player);

            // 获取手中换牌类型的牌 (只需要三张)
            for (Integer card : player.getHandsCards()) {
                if (AdaptionAlgorithm.typeOf(card).equals(exchangeType)) {
                    exchange.add(card);
                }
                if (exchange.size() == 3) {
                    break;
                }
            }

            // 推送给玩家
            player.setExchange(exchange);

            // 推送消息给玩家 换牌后的手牌
            Connection.broadcaster().send(player.getRoomNum(), player.getUserId(), AuxiliaryRoom.pushExchangedData(player));

            List<? extends Player> players = RoomManager.obtain(roomNum).getPlayers();

            // 换三张是否结束
            if (AuxiliaryRoom.exchangeOver(players)) {
                // 随机换牌方向
                ExchangeThree.Direction direction = Enum.valueOf(ExchangeThree.Direction.class,
                        d -> d.ordinal() == RandomUtil.randomInt(3));

                // 换牌
                AuxiliaryRoom.exchangeThree(direction, players);

                // 换牌结束 (方法中已经推送消息，无需多次推送)
                AuxiliaryRoom.exchangeFinish(roomNum, direction);
            }

        }, timeoutProperties.getExchangeTimeout());

        MemberTimer memberTimer = MemberTimer.builder()
                .memberId(player.getUserId())
                .timer(timer)
                .build();

        // 添加任务
        TaskManager.add(multiExchangeThree, memberTimer);
    }

    /**
     * 通知玩家出牌 （这里只是通知玩家出牌，所以没有碰和点杠的情况）
     */
    public void notifiesDiscard(Player p) {

        MahjongPlayer player = (MahjongPlayer) p;
        // 手牌
        List<Integer> handCards = player.getHandsCards();

        // 可操作性对象 (此时已包含出牌操作)
        MahjongModel.CanOperateResult.Builder builder = AuxiliaryRoom.notifiesDiscard(player);

        // 听牌信息
        List<Integer> readyHand = AuxiliaryRoom.checkReadyHand(player, builder);
        if (Utils.nonEmpty(readyHand)) {
            // 推送听牌操作
            builder.addOperateModel(AuxiliaryRoom.canOperate(MahjongModel.OptionsType.READY_HAND, readyHand));
        }

        // 检查杠
        List<Integer> kong = AdaptionAlgorithm.checkKong(handCards);
        if (!kong.isEmpty()) {
            // 杠操作
            builder.addOperateModel(AuxiliaryRoom.canOperate(MahjongModel.OptionsType.KONG, kong));
        }

        // 推送可操作对象
        Packet packet = Packet.newPacket(MessageCmdEnum.Command.CAN_OPERATE, builder.build().toByteArray());
        Connection.broadcaster().send(player.getRoomNum(), player.getUserId(), packet);

        // 开启出牌定时任务
        discardTask(player);
    }

    /**
     * 出牌定时任务
     *
     * @param player 玩家信息
     */
    public void discardTask(MahjongPlayer player) {

        // 定时任务的名称
        String name = String.format(TaskManager.TaskNames.discardName, player.getUserId().toString());

        // 10秒后自动出牌
        Timer timer = TaskScheduler.submit(() -> {
            // 自动出牌时，只出最后一张
            Integer card = player.getLastCard();

            MahjongModel.PlayerOperateResult.OperateInfoModel.Builder oimBuilder = MahjongModel.PlayerOperateResult.OperateInfoModel.newBuilder();
            oimBuilder.addAllOperateCard(player.getHandsCards())
                    .setBeOperatePlayerSeat(player.getPosition())     // 这时候推送的是自己的位置 (自己的轮子)
                    .setType(MahjongModel.OptionsType.DISCARD)
                    .setBeOperateCard(card);

            MahjongModel.PlayerOperateResult.Builder builder = MahjongModel.PlayerOperateResult.newBuilder();
            builder.setSeatNum(player.getPosition())
                    .setOperateInfoModel(oimBuilder);

            // 推送自动出牌
            Connection.broadcaster().notify(player.getRoomNum(), AuxiliaryRoom.discard(player, builder.build()));

            // 检查是否还有操作
            checkOperate(player.getRoomNum(), builder.build());

        }, timeoutProperties.getDiscardTimeout());

        // 添加到任务管理器
        TaskManager.set(name, timer);
    }

    /**
     * 检查是否有玩家可以对轮子轮到的玩家打出的牌进行操作
     *
     * @param roomNum       房间号
     * @param operateResult 操作结果
     * @param packet        操作结果数据包
     */
    public void checkOperate(Long roomNum, MahjongModel.PlayerOperateResult operateResult, Packet packet) {
        // 房间信息
        RoomDetail roomDetail = RoomManager.obtain(roomNum);

        MahjongModel.PlayerOperateResult.OperateInfoModel model = operateResult.getOperateInfoModel();

        // 被操作的牌
        Integer beCard = model.getBeOperateCard();

        // 被操作者位置
        int beSeatNum = model.getBeOperatePlayerSeat();

        // 游戏类型
        GameType type = GameType.valueOf(roomDetail.getGameType().intValue());

        // 构造操作对象
        MahjongModel.CanOperateResult.Builder builder = MahjongModel.CanOperateResult.newBuilder();

        boolean flag = true;

        for (Player player : roomDetail.getPlayers()) {
            MahjongPlayer mp = (MahjongPlayer) player;

            if (operateResult.getOperateInfoModel().getType().equals(MahjongModel.OptionsType.WINNER)) {
                // 如果有玩家胡了 就只需要验证其余玩家能不能胡
                if (mp.isWinning()) {
                    flag = false;
                    continue;
                }
            }

            // 自己的轮子不用判定
            if (roomDetail.getDiscardSeat() == mp.getPosition()) {
                continue;
            }

            // 血战的情况下 已经胡了的 也不用判定
            if (type.equals(GameType.XZ)) {
                if (mp.isWined()) {
                    continue;
                }
            }

            // 已经听牌了
            if (mp.isReadyHand() || AuxiliaryRoom.isReadyHand(mp, beCard)) {
                if (mp.getReadyHandMap().containsKey(beCard)) {
                    // 推送胡牌操作
                    builder.setSeatNum(player.getPosition())
                            .addOperateModel(AuxiliaryRoom.canOperate(MahjongModel.OptionsType.WINNER, Collections.singletonList(beCard)));

                    mp.setWinning(Constants.YES);
                }
            }

            // 未胡牌才检查碰和杠
            if (!mp.isWined()) {
                List<Integer> newHandCard = new ArrayList<>(mp.getHandsCards());

                // 检查杠和碰
                // 四川麻将的情况下
                if (type.equals(GameType.XL) || type.equals(GameType.XZ)) {

                    // 打出的牌不是自己的定缺牌的情况下
                    if (!AdaptionAlgorithm.typeOf(beCard).equals(mp.getUseless())) {
                        AuxiliaryRoom.checkSpecificCardAndAdd(builder, newHandCard, beCard, mp);
                    }
                } else {
                    AuxiliaryRoom.checkSpecificCardAndAdd(builder, newHandCard, beCard, mp);
                }
            }

            if (!builder.getOperateModelList().isEmpty()) {
                // 有操作的情况下才可以过
                builder.addOperateModel(AuxiliaryRoom.canOperate(MahjongModel.OptionsType.PASS, Collections.singletonList(beCard)));
                // 推送玩家操作
                Connection.broadcaster().send(player.getRoomNum(), player.getUserId(), AuxiliaryRoom.pushOperate(builder));
                // 启动定时任务
                memberOperateTask(player, beSeatNum, builder.build(), type);
                // 标记有玩家可操作
                flag = false;
            }

        }

        // 没有玩家进行操作
        if (flag) {

            // 不是出牌的情况下 还要通知其余牌友玩家的操作
            if (!model.getType().equals(MahjongModel.OptionsType.DISCARD)
                    && Objects.nonNull(packet)) {

                Connection.broadcaster().notify(roomNum, packet);
            }

            // 旋转轮子
            updateDiscardSeatNum(roomNum);
        }
    }

    /**
     * 检查是否有玩家可以对轮子轮到的玩家打出的牌进行操作
     *
     * @param roomNum       房间号
     * @param operateResult 操作结果
     */
    public void checkOperate(Long roomNum, MahjongModel.PlayerOperateResult operateResult) {
        checkOperate(roomNum, operateResult, null);
    }

    /**
     * 玩家操作类型定时任务 (只判定玩家胡，其余的操作一概过)
     *
     * @param operator       玩家信息 (操作者)
     * @param seatNum        被操作的座位号
     * @param memberOperates 玩家所有可操作类型
     */
    public void memberOperateTask(Player operator, int seatNum, MahjongModel.CanOperateResult memberOperates, GameType type) {
        String name = String.format(TaskManager.TaskNames.memberOperateName, operator.getUserId());

        Timer timer = TaskScheduler.submit(() -> {

            for (MahjongModel.CanOperateResult.OperateModel model : memberOperates.getOperateModelList()) {

                if (type == GameType.XZ) {
                    if (((MahjongPlayer) operator).isWined()) {
                        // 血战模式下，玩家已胡牌后不能进行任何操作
                        break;
                    }
                }

                // 被操作的牌
                Integer beOperateCard = model.getCardList().get(0);

                MahjongModel.PlayerOperateResult.OperateInfoModel.Builder oimBuilder = MahjongModel.PlayerOperateResult.OperateInfoModel.newBuilder();
                oimBuilder.setBeOperateCard(beOperateCard)
                        .setBeOperatePlayerSeat(seatNum);

                MahjongModel.PlayerOperateResult.Builder builder = MahjongModel.PlayerOperateResult.newBuilder();
                builder.setSeatNum(operator.getPosition())
                        .setOperateInfoModel(oimBuilder);

                // 帮玩家自动胡牌
                if (model.getType() == MahjongModel.OptionsType.WINNER) {

                    oimBuilder.setType(MahjongModel.OptionsType.WINNER);

                    // 胡牌
                    Packet winner = AuxiliaryRoom.winner((MahjongPlayer) operator, builder.build());
                    Connection.broadcaster().notify(operator.getRoomNum(), winner);

                    // 指定轮子的位置
                    updateDiscardSeatNum(operator.getRoomNum(), AuxiliaryRoom.nextDealer(operator));
                    break;
                }

                // 其余操作一律过
                Connection.broadcaster().notify(operator.getRoomNum(), AuxiliaryRoom.pass(builder.build()));
            }

            // 旋转轮子
            updateDiscardSeatNum(operator.getRoomNum());

        }, timeoutProperties.getSpecialTimeout());

        TaskManager.set(name, timer);
    }

    /**
     * {@link #updateDiscardSeatNum(Long, Integer)}
     *
     * @param roomNum 房间号
     */
    public void updateDiscardSeatNum(Long roomNum) {
        updateDiscardSeatNum(roomNum, -1);
    }

    /**
     * 更新下一个摸牌的位置 （并通知他出牌）
     *
     * @param roomNum 房间号
     * @param seatNum 下一个位置 (可能是空的，为空时需要根据上一次出牌的指针来判定下一位出牌的玩家)
     */
    public void updateDiscardSeatNum(Long roomNum, Integer seatNum) {
        RoomDetail roomDetail = RoomManager.obtain(roomNum);

        int nextDiscardSeat = Objects.isNull(seatNum) || seatNum < 0 ? roomDetail.getDiscardSeat() : seatNum;

        AtomicInteger discardSeat = new AtomicInteger(nextDiscardSeat);

        // 游戏类型
        GameType type = GameType.valueOf(roomDetail.getGameType().intValue());

        // 下一个轮子
        discardSeat.incrementAndGet();

        // 轮询房间内所有玩家 (血战模式下需要轮询玩家是否胡牌)
        if (type == GameType.XZ) {
            for (Player p : roomDetail.getPlayers()) {
                if (!((MahjongPlayer) p).isWined()) {
                    roomDetail.setDiscardSeat(discardSeat.get() % 4);
                    break;
                }
            }
        } else {
            roomDetail.setDiscardSeat(discardSeat.get() % 4);
        }

        // 摸牌
        Utils.accept(roomDetail.getPlayers(), player -> player.getPosition() == seatNum, this::drawAndNotifiesDiscard);
    }

    /**
     * 房间超时定时任务
     *
     * @param roomNum 房间号
     */
    public void roomTimeoutTask(Long roomNum) {
        // 避免缓存浪费 一个小时后自动解散房间
        Timer timer = TaskScheduler.submit(() ->
                        Connection.broadcaster().notify(roomNum, AuxiliaryRoom.dissolved(roomNum)),
                timeoutProperties.getRoomTimeout());

        String name = String.format(TaskManager.TaskNames.room, roomNum.toString());
        TaskManager.set(name, timer);
    }

    /**
     * 清除玩家操作状态
     *
     * @param player 玩家信息
     */
    public void clearOperateStatue(MahjongPlayer player) {
        player.setWinning(Constants.NO);
        player.setKong(Constants.NO);
        player.setPong(Constants.NO);
    }

    // -----------------------------------------  private ----------------------------------------

    /**
     * 摸牌并通知玩家出牌
     *
     * @param p 玩家信息
     */
    private void drawAndNotifiesDiscard(Player p) {
        RoomDetail roomDetail = RoomManager.obtain(p.getRoomNum());
        roomDetail.draw(p);
        notifiesDiscard(p);
    }

    private void cancel(String name) {
        // 获取定时任务
        Timer timer = TaskManager.get(name);
        if (Objects.isNull(timer)) {
            log.error("定时任务未找到 , 任务名称: {}", name);
            throw new TimerTaskNotFoundException("定时任务未找到");
        }
        // 取消执行
        timer.cancel();
    }

    private String parsePath(Long gameType) {
        // TODO 游戏类型处理
//        SpecificTaskTransferClient client = SimpleBeanFactory.getBean(SpecificTaskTransferClient.class);
//        ResponseResult<GamesMode> result = client.getType(gameType);
//
//        if(!result.getCode().equals(ResponseCode.SUCCESS.getCode())){
//            throw new GameNotFoundException("未识别的游戏类型: " + gameType);
//        }
//
//        if(result.getData().getParentId().equals(0L)){
//            gameType = result.getData().getParentId();
//        }

        if (gameType == 1) {
            return Constants.MAHJONG_PRE_PATH;
        } else if (gameType == 2) {
            return Constants.POKER_PRE_PATH;
        }
        log.error("post 执行失败，执行操作类型错误: [ {} ]", gameType, new GameNotFoundException("未识别的游戏类型: " + gameType));
        return Constant.empty;
    }

    /**
     * 验证房间是否有效
     */
    private void verificationRoom(Long roomNum) {
        if (!RoomManager.hasRoomNum(roomNum)) {

            // 解散房间
            Connection.broadcaster().notify(roomNum, AuxiliaryRoom.dissolved(roomNum));

            // 抛出异常终止
            String msg = String.format("房间缓存过期 房间号: %s", roomNum);
            BusinessException ex = new BusinessException(msg);
            log.error(ex.getMessage(), ex);
            throw ex;
        }
    }

}
